Arbeiten mit Serializern und ModelSerializer
Serializers • Validierung • Relations • Nested Objects
Was sind Serializers?
Warum brauchen wir sie?
Unterschiede verstehen
Wann was nutzen?
Field-Level Validation
Object-Level Validation
Custom Validators
ForeignKey Relations
Many-to-Many
Nested Serializers
SerializerMethodField
Custom Fields
Best Practices
Konvertiert zwischen komplexen Datentypen und nativen Python-Typen
# 1. Serialisierung (Read - GET)
Python Object → Serializer → JSON
Movie(id=1, title="Matrix") → {"id": 1, "title": "Matrix"}
# 2. Deserialisierung (Write - POST/PUT)
JSON → Serializer → Python Object
{"title": "Inception"} → Movie(title="Inception")
# Manuell JSON erstellen
def movie_to_json(movie):
return {
'id': movie.id,
'title': movie.title,
'year': movie.year,
# ... für jedes Feld
}
# Manuell validieren
def validate_movie(data):
if not data.get('title'):
raise ValueError("Title required")
if data.get('year') < 1888:
raise ValueError("Year too early")
# ... für jedes Feld
# Automatisch!
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
# Verwendung:
serializer = MovieSerializer(movie)
json_data = serializer.data # Fertig!
# Validierung automatisch:
serializer = MovieSerializer(data=request.data)
if serializer.is_valid():
serializer.save() # Fertig!
Basis-Serializer vs. Model-basierter Serializer
Für nicht-Model Daten
from rest_framework import serializers
class MovieSerializer(serializers.Serializer):
"""Basis Serializer - Alles manuell"""
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
year = serializers.IntegerField()
rating = serializers.DecimalField(
max_digits=3,
decimal_places=1,
required=False
)
def create(self, validated_data):
"""Manuell erstellen"""
return Movie.objects.create(**validated_data)
def update(self, instance, validated_data):
"""Manuell aktualisieren"""
instance.title = validated_data.get('title', instance.title)
instance.year = validated_data.get('year', instance.year)
instance.rating = validated_data.get('rating', instance.rating)
instance.save()
return instance
Für Django Models
from rest_framework import serializers
class MovieSerializer(serializers.ModelSerializer):
"""ModelSerializer - Automatisch!"""
class Meta:
model = Movie
fields = '__all__'
# Oder:
# fields = ['id', 'title', 'year', 'rating']
# exclude = ['created_at']
read_only_fields = ['id', 'created_at', 'updated_at']
# Fertig! create() und update() automatisch!
# Validierung basierend auf Model-Definition!
# Felder automatisch aus Model!
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__' # Alle Model-Felder
# Output:
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": "8.7",
"description": "...",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = ['id', 'title', 'year', 'rating']
# Nur diese Felder!
# Output:
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"rating": "8.7"
}
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
exclude = ['created_at', 'updated_at']
# Alle außer diese!
# Output:
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": "8.7",
"description": "..."
}
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
# Diese können NICHT geändert werden
# Bei POST/PUT ignoriert:
{
"id": 999, # ← Ignoriert!
"title": "New Movie",
"year": 2024
}
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
extra_kwargs = {
'title': {'required': True, 'min_length': 3},
'year': {'min_value': 1888, 'max_value': 2100},
'rating': {'required': False},
'description': {'write_only': True} # Nur bei POST/PUT
}
class MovieCastingSerializer(serializers.ModelSerializer):
class Meta:
model = MovieCasting
fields = '__all__'
depth = 1 # Auto-expand ForeignKeys!
# Output:
{
"id": 1,
"movie": { # ← Automatisch verschachtelt!
"id": 1,
"title": "The Matrix"
},
"artist": {
"id": 1,
"first_name": "Keanu",
"last_name": "Reeves"
},
"role_name": "Neo"
}
Von einfach zu komplex
Einzelne Felder validieren
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def validate_year(self, value):
"""Validiere year-Feld"""
if value < 1888:
raise serializers.ValidationError(
"Erstes Film war 1888!"
)
if value > 2100:
raise serializers.ValidationError(
"Jahr zu weit in der Zukunft!"
)
return value
def validate_title(self, value):
"""Validiere title-Feld"""
if len(value) < 2:
raise serializers.ValidationError(
"Titel zu kurz!"
)
if Movie.objects.filter(title__iexact=value).exists():
raise serializers.ValidationError(
"Film existiert bereits!"
)
return value
Methode: validate_<field_name>(self, value)
Mehrere Felder zusammen validieren
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def validate(self, attrs):
"""Validiere das gesamte Objekt"""
# attrs = dict mit allen Feldern
# Beispiel: Jahr und Rating zusammen prüfen
year = attrs.get('year')
rating = attrs.get('rating')
if year and year < 1920 and rating and rating > 9.0:
raise serializers.ValidationError(
"Stummfilme können nicht so hoch bewertet sein!"
)
# Beispiel: Genre und Titel zusammen
genre = attrs.get('genre')
title = attrs.get('title')
if genre == 'Horror' and 'Kids' in title:
raise serializers.ValidationError(
"Horror-Filme nicht für Kinder!"
)
return attrs # Wichtig: attrs zurückgeben!
Methode: validate(self, attrs)
Wiederverwendbare Validatoren
# filepath: movies/validators.py
from rest_framework import serializers
def validate_year_range(value):
"""Wiederverwendbarer Validator"""
if value < 1888 or value > 2100:
raise serializers.ValidationError(
f"Jahr muss zwischen 1888 und 2100 liegen, nicht {value}"
)
def validate_rating_range(value):
"""Rating-Validator"""
if value < 0 or value > 10:
raise serializers.ValidationError(
"Rating muss zwischen 0 und 10 liegen"
)
# filepath: movies/serializers.py
class MovieSerializer(serializers.ModelSerializer):
# Validators direkt am Feld
year = serializers.IntegerField(validators=[validate_year_range])
rating = serializers.DecimalField(
max_digits=3,
decimal_places=1,
validators=[validate_rating_range],
required=False
)
class Meta:
model = Movie
fields = '__all__'
Wiederverwendbar in mehreren Serializern!
# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie
from .validators import validate_year_range, validate_rating_range
from datetime import datetime
class MovieSerializer(serializers.ModelSerializer):
"""
Movie Serializer mit umfassender Validierung
"""
# Custom Validators
year = serializers.IntegerField(validators=[validate_year_range])
rating = serializers.DecimalField(
max_digits=3,
decimal_places=1,
validators=[validate_rating_range],
required=False,
allow_null=True
)
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
extra_kwargs = {
'title': {
'required': True,
'min_length': 2,
'max_length': 200
},
'description': {
'required': False,
'allow_blank': True
}
}
# Field-Level Validation
def validate_title(self, value):
"""Titel darf nicht bereits existieren"""
# Bei Update: Eigenen Film ausschließen
if self.instance:
# Update-Modus
if Movie.objects.filter(
title__iexact=value
).exclude(pk=self.instance.pk).exists():
raise serializers.ValidationError(
f"Film mit Titel '{value}' existiert bereits!"
)
else:
# Create-Modus
if Movie.objects.filter(title__iexact=value).exists():
raise serializers.ValidationError(
f"Film mit Titel '{value}' existiert bereits!"
)
return value.strip() # Leerzeichen entfernen
def validate_genre(self, value):
"""Genre muss in erlaubter Liste sein"""
allowed_genres = [
'Action', 'Comedy', 'Drama', 'Horror',
'Sci-Fi', 'Romance', 'Thriller', 'Documentary'
]
if value and value not in allowed_genres:
raise serializers.ValidationError(
f"Genre muss eines sein von: {', '.join(allowed_genres)}"
)
return value
# Object-Level Validation
def validate(self, attrs):
"""Gesamtes Objekt validieren"""
year = attrs.get('year')
rating = attrs.get('rating')
genre = attrs.get('genre')
title = attrs.get('title', '')
# Jahr darf nicht in der Zukunft liegen
current_year = datetime.now().year
if year and year > current_year + 1:
raise serializers.ValidationError({
'year': f'Jahr darf nicht mehr als {current_year + 1} sein'
})
# Alte Filme können nicht so hoch bewertet sein
if year and year < 1920 and rating and rating > 9.5:
raise serializers.ValidationError(
'Stummfilme vor 1920 können maximal 9.5 haben'
)
# Genre und Titel Kombination
if genre == 'Documentary' and rating and rating > 9.0:
# Warnung, aber kein Fehler
pass
# Horror nicht für Kids
if genre == 'Horror' and any(word in title.lower() for word in ['kids', 'children', 'family']):
raise serializers.ValidationError(
'Horror-Filme nicht für Kinder geeignet'
)
return attrs
# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import MovieSerializer
class MovieCreateView(APIView):
def post(self, request):
"""Film erstellen mit Fehlerbehandlung"""
serializer = MovieSerializer(data=request.data)
# Validierung
if serializer.is_valid():
# ✅ Daten OK → Speichern
movie = serializer.save()
return Response(
serializer.data,
status=status.HTTP_201_CREATED
)
else:
# ❌ Validierungsfehler → Error Response
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
# Field-Level Error:
{
"year": [
"Jahr muss zwischen 1888 und 2100 liegen, nicht 1800"
],
"title": [
"Film mit Titel 'Matrix' existiert bereits!"
]
}
# Object-Level Error:
{
"non_field_errors": [
"Stummfilme vor 1920 können maximal 9.5 haben"
]
}
# Multiple Errors:
{
"title": [
"Dieses Feld ist erforderlich."
],
"year": [
"Jahr muss zwischen 1888 und 2100 liegen, nicht 2200"
],
"rating": [
"Rating muss zwischen 0 und 10 liegen"
]
}
is_valid() vor save() aufrufen5 verschiedene Ansätze
# models.py
class MovieCasting(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
role_name = models.CharField(max_length=200)
# serializers.py
class MovieCastingSerializer(serializers.ModelSerializer):
class Meta:
model = MovieCasting
fields = '__all__'
# Output:
{
"id": 1,
"movie": 1, # ← Nur ID!
"artist": 5, # ← Nur ID!
"role_name": "Neo"
}
✅ Vorteile: Einfach, klein
❌ Nachteile: Client muss extra Request machen
class MovieCastingSerializer(serializers.ModelSerializer):
movie = serializers.StringRelatedField()
artist = serializers.StringRelatedField()
class Meta:
model = MovieCasting
fields = '__all__'
# models.py braucht __str__:
class Movie(models.Model):
def __str__(self):
return self.title
# Output:
{
"id": 1,
"movie": "The Matrix", # ← __str__() Ausgabe
"artist": "Keanu Reeves", # ← __str__() Ausgabe
"role_name": "Neo"
}
✅ Vorteile: Lesbar
❌ Nachteile: Read-only, keine IDs
class MovieCastingSerializer(serializers.ModelSerializer):
movie = serializers.SlugRelatedField(
slug_field='title', # Welches Feld?
queryset=Movie.objects.all()
)
artist = serializers.SlugRelatedField(
slug_field='last_name',
queryset=Artist.objects.all()
)
class Meta:
model = MovieCasting
fields = '__all__'
# Output:
{
"id": 1,
"movie": "The Matrix", # ← title-Feld
"artist": "Reeves", # ← last_name-Feld
"role_name": "Neo"
}
✅ Vorteile: Lesbar, beschreibbar
❌ Nachteile: Feld muss unique sein
class MovieCastingSerializer(serializers.ModelSerializer):
movie = serializers.HyperlinkedRelatedField(
view_name='movie-detail',
queryset=Movie.objects.all()
)
class Meta:
model = MovieCasting
fields = '__all__'
# Output:
{
"id": 1,
"movie": "http://api.example.com/movies/1/", # ← URL!
"artist": "http://api.example.com/artists/5/",
"role_name": "Neo"
}
✅ Vorteile: RESTful, direkt klickbar
❌ Nachteile: Längere URLs
class MovieCastingSerializer(serializers.ModelSerializer):
movie = MovieSerializer(read_only=True)
artist = ArtistSerializer(read_only=True)
# Für Write: IDs
movie_id = serializers.PrimaryKeyRelatedField(
source='movie',
queryset=Movie.objects.all(),
write_only=True
)
artist_id = serializers.PrimaryKeyRelatedField(
source='artist',
queryset=Artist.objects.all(),
write_only=True
)
class Meta:
model = MovieCasting
fields = '__all__'
# Output (Read):
{
"id": 1,
"movie": { # ← Verschachtelt!
"id": 1,
"title": "The Matrix",
"year": 1999
},
"artist": {
"id": 5,
"first_name": "Keanu",
"last_name": "Reeves"
},
"role_name": "Neo"
}
# Input (Write):
{
"movie_id": 1, # ← Nur IDs beim Schreiben
"artist_id": 5,
"role_name": "Neo"
}
✅ Vorteile: Volle Daten, best UX
❌ Nachteile: Größere Response
Komplexe Datenstrukturen elegant darstellen
# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting
# 1. Artist Serializer (Basis)
class ArtistSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = Artist
fields = ['id', 'first_name', 'last_name', 'full_name', 'birth_date']
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}"
# 2. MovieCasting Serializer (mit Artist nested)
class MovieCastingDetailSerializer(serializers.ModelSerializer):
artist = ArtistSerializer(read_only=True) # ← Nested!
class Meta:
model = MovieCasting
fields = ['id', 'artist', 'role_name', 'is_lead']
# 3. Movie Serializer (mit Castings nested)
class MovieDetailSerializer(serializers.ModelSerializer):
# Reverse Relation: related_name='castings'
castings = MovieCastingDetailSerializer(many=True, read_only=True)
casting_count = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = '__all__'
def get_casting_count(self, obj):
return obj.castings.count()
GET /api/movies/1/
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": "8.7",
"casting_count": 3,
"castings": [ # ← Nested Array!
{
"id": 1,
"artist": { # ← Doppelt verschachtelt!
"id": 5,
"first_name": "Keanu",
"last_name": "Reeves",
"full_name": "Keanu Reeves",
"birth_date": "1964-09-02"
},
"role_name": "Neo",
"is_lead": true
},
{
"id": 2,
"artist": {
"id": 6,
"first_name": "Laurence",
"last_name": "Fishburne",
"full_name": "Laurence Fishburne",
"birth_date": "1961-07-30"
},
"role_name": "Morpheus",
"is_lead": true
},
{
"id": 3,
"artist": {
"id": 7,
"first_name": "Carrie-Anne",
"last_name": "Moss",
"full_name": "Carrie-Anne Moss",
"birth_date": "1967-08-21"
},
"role_name": "Trinity",
"is_lead": true
}
]
}
DRF macht Nested READ automatisch, aber WRITE nicht!
class MovieDetailSerializer(serializers.ModelSerializer):
castings = MovieCastingDetailSerializer(many=True, read_only=True)
# ^^^^^^^^^ Read-Only!
class Meta:
model = Movie
fields = '__all__'
# POST geht NICHT:
{
"title": "New Movie",
"castings": [
{"artist_id": 1, "role_name": "Hero"} # ← Ignoriert!
]
}
class MovieDetailSerializer(serializers.ModelSerializer):
castings = MovieCastingDetailSerializer(many=True, read_only=True)
# Separate Felder für Write
casting_data = serializers.ListField(
child=serializers.DictField(),
write_only=True,
required=False
)
class Meta:
model = Movie
fields = '__all__'
def create(self, validated_data):
"""Custom Create mit Nested Objects"""
# 1. Casting-Daten extrahieren
casting_data = validated_data.pop('casting_data', [])
# 2. Movie erstellen
movie = Movie.objects.create(**validated_data)
# 3. Castings erstellen
for casting in casting_data:
MovieCasting.objects.create(
movie=movie,
artist_id=casting['artist_id'],
role_name=casting['role_name'],
is_lead=casting.get('is_lead', False)
)
return movie
def update(self, instance, validated_data):
"""Custom Update mit Nested Objects"""
# 1. Casting-Daten extrahieren
casting_data = validated_data.pop('casting_data', None)
# 2. Movie aktualisieren
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
# 3. Castings ersetzen (optional)
if casting_data is not None:
# Alte löschen
instance.castings.all().delete()
# Neue erstellen
for casting in casting_data:
MovieCasting.objects.create(
movie=instance,
artist_id=casting['artist_id'],
role_name=casting['role_name'],
is_lead=casting.get('is_lead', False)
)
return instance
# Verwendung:
POST /api/movies/
{
"title": "Inception",
"year": 2010,
"casting_data": [ # ← Nested erstellen!
{"artist_id": 10, "role_name": "Cobb", "is_lead": true},
{"artist_id": 11, "role_name": "Ariadne", "is_lead": true}
]
}
# Besser: Zwei getrennte Requests
# 1. Movie erstellen
POST /api/movies/
{
"title": "Inception",
"year": 2010
}
# Response: {"id": 42, "title": "Inception", ...}
# 2. Castings hinzufügen
POST /api/movies/42/castings/
{
"artist_id": 10,
"role_name": "Cobb",
"is_lead": true
}
POST /api/movies/42/castings/
{
"artist_id": 11,
"role_name": "Ariadne",
"is_lead": true
}
Vorteile:
# filepath: movies/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=True, methods=['post'])
def add_casting(self, request, pk=None):
"""Custom Action für Casting hinzufügen"""
movie = self.get_object()
# Validierung
artist_id = request.data.get('artist_id')
role_name = request.data.get('role_name')
is_lead = request.data.get('is_lead', False)
if not artist_id or not role_name:
return Response(
{'error': 'artist_id und role_name required'},
status=400
)
# Casting erstellen
casting = MovieCasting.objects.create(
movie=movie,
artist_id=artist_id,
role_name=role_name,
is_lead=is_lead
)
serializer = MovieCastingSerializer(casting)
return Response(serializer.data, status=201)
# Verwendung:
POST /api/movies/42/add_casting/
{
"artist_id": 10,
"role_name": "Cobb",
"is_lead": true
}
Grund: Einfacherer Code, bessere Wartbarkeit, klarere API
Nutze Nested Serializers für READ (viele Daten auf einmal)
Nutze Separate Endpoints für WRITE (klarere Struktur)
Felder die nicht im Model existieren
# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie
from datetime import datetime
class MovieSerializer(serializers.ModelSerializer):
# Berechnete Felder
age = serializers.SerializerMethodField()
is_classic = serializers.SerializerMethodField()
rating_category = serializers.SerializerMethodField()
casting_count = serializers.SerializerMethodField()
lead_actors = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = [
'id', 'title', 'year', 'rating',
'age', 'is_classic', 'rating_category', # ← Custom!
'casting_count', 'lead_actors'
]
def get_age(self, obj):
"""Alter des Films in Jahren"""
current_year = datetime.now().year
return current_year - obj.year
def get_is_classic(self, obj):
"""Ist der Film ein Klassiker? (älter als 30 Jahre)"""
return self.get_age(obj) > 30
def get_rating_category(self, obj):
"""Rating-Kategorie basierend auf Bewertung"""
if not obj.rating:
return "Nicht bewertet"
rating = float(obj.rating)
if rating >= 9.0:
return "Meisterwerk"
elif rating >= 8.0:
return "Ausgezeichnet"
elif rating >= 7.0:
return "Gut"
elif rating >= 6.0:
return "Durchschnittlich"
else:
return "Schwach"
def get_casting_count(self, obj):
"""Anzahl der Besetzungen"""
return obj.castings.count()
def get_lead_actors(self, obj):
"""Liste der Hauptdarsteller"""
lead_castings = obj.castings.filter(is_lead=True)
return [
{
'name': f"{c.artist.first_name} {c.artist.last_name}",
'role': c.role_name
}
for c in lead_castings
]
GET /api/movies/1/
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"rating": "8.7",
"age": 25, # ← Berechnet!
"is_classic": false, # ← Berechnet!
"rating_category": "Ausgezeichnet", # ← Berechnet!
"casting_count": 3, # ← Berechnet!
"lead_actors": [ # ← Berechnet!
{"name": "Keanu Reeves", "role": "Neo"},
{"name": "Laurence Fishburne", "role": "Morpheus"},
{"name": "Carrie-Anne Moss", "role": "Trinity"}
]
}
get_<field_name>(self, obj)SerializerMethodField kann langsam sein bei Listen
class MovieSerializer(serializers.ModelSerializer):
casting_count = serializers.SerializerMethodField()
lead_actors = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = ['id', 'title', 'casting_count', 'lead_actors']
def get_casting_count(self, obj):
# ❌ Query für jedes Movie!
return obj.castings.count()
def get_lead_actors(self, obj):
# ❌ Query für jedes Movie!
lead_castings = obj.castings.filter(is_lead=True)
return [c.artist.first_name for c in lead_castings]
# Problem bei Liste:
GET /api/movies/ # 100 Filme
# → 1 Query für Movies
# → 100 Queries für casting_count
# → 100 Queries für lead_actors
# = 201 Queries! 😱
# 1. View optimieren:
from django.db.models import Count, Prefetch
class MovieViewSet(viewsets.ModelViewSet):
serializer_class = MovieSerializer
def get_queryset(self):
"""QuerySet optimieren"""
return Movie.objects.annotate(
# Casting-Count direkt in DB berechnen
casting_count_db=Count('castings')
).prefetch_related(
# Lead Castings vorab laden
Prefetch(
'castings',
queryset=MovieCasting.objects.filter(
is_lead=True
).select_related('artist')
)
)
# 2. Serializer nutzt Prefetch:
class MovieSerializer(serializers.ModelSerializer):
casting_count = serializers.IntegerField(
source='casting_count_db', # ← Aus Annotation!
read_only=True
)
lead_actors = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = ['id', 'title', 'casting_count', 'lead_actors']
def get_lead_actors(self, obj):
# ✅ Verwendet prefetch_related, keine extra Query!
return [
c.artist.first_name
for c in obj.castings.all() # Schon geladen!
if c.is_lead
]
# Resultat:
GET /api/movies/ # 100 Filme
# → 1 Query für Movies + Annotation
# → 1 Query für prefetch_related
# = 2 Queries! 🚀
annotate() für Berechnungen in DBselect_related() für ForeignKeysprefetch_related() für Many-to-Many/Reverse Relationsonly() / defer() für weniger FelderRead: Viele Daten, Write: Nur Nötige
# filepath: movies/serializers.py
# 1. LIST: Minimal (für Übersicht)
class MovieListSerializer(serializers.ModelSerializer):
"""Minimal für Listen"""
class Meta:
model = Movie
fields = ['id', 'title', 'year', 'rating']
# 2. DETAIL: Maximal (für Einzelansicht)
class MovieDetailSerializer(serializers.ModelSerializer):
"""Maximal für Details mit Nested Objects"""
castings = MovieCastingDetailSerializer(many=True, read_only=True)
casting_count = serializers.SerializerMethodField()
age = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = '__all__'
def get_casting_count(self, obj):
return obj.castings.count()
def get_age(self, obj):
from datetime import datetime
return datetime.now().year - obj.year
# 3. CREATE/UPDATE: Nur beschreibbare Felder
class MovieWriteSerializer(serializers.ModelSerializer):
"""Nur für Create/Update"""
class Meta:
model = Movie
fields = ['title', 'year', 'genre', 'rating', 'description']
extra_kwargs = {
'title': {'required': True},
'year': {'required': True},
}
# Validierung...
def validate_year(self, value):
if value < 1888:
raise serializers.ValidationError("Jahr zu früh")
return value
# filepath: movies/views.py
from rest_framework import viewsets
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
def get_serializer_class(self):
"""Dynamisch Serializer wählen"""
if self.action == 'list':
return MovieListSerializer # Minimal
elif self.action in ['create', 'update', 'partial_update']:
return MovieWriteSerializer # Nur Schreibfelder
else: # retrieve
return MovieDetailSerializer # Maximal
# Resultat:
GET /api/movies/ # → MovieListSerializer (klein)
GET /api/movies/1/ # → MovieDetailSerializer (groß, nested)
POST /api/movies/ # → MovieWriteSerializer (nur Pflichtfelder)
PUT /api/movies/1/ # → MovieWriteSerializer
PATCH /api/movies/1/ # → MovieWriteSerializer
Wenn Standard-Felder nicht ausreichen
# filepath: movies/fields.py
from rest_framework import serializers
from datetime import timedelta
class DurationField(serializers.Field):
"""Custom Field für Zeitdauer in Minuten"""
def to_representation(self, value):
"""Python timedelta → JSON String"""
if value is None:
return None
# timedelta → Minuten
total_minutes = int(value.total_seconds() / 60)
hours = total_minutes // 60
minutes = total_minutes % 60
if hours > 0:
return f"{hours}h {minutes}min"
else:
return f"{minutes}min"
def to_internal_value(self, data):
"""JSON String → Python timedelta"""
if data is None:
return None
# Parse verschiedene Formate
import re
# Format: "120min" oder "2h 30min"
pattern = r'(?:(\d+)h\s*)?(?:(\d+)min)?'
match = re.match(pattern, str(data))
if not match:
raise serializers.ValidationError(
"Format muss sein: '120min' oder '2h 30min'"
)
hours = int(match.group(1) or 0)
minutes = int(match.group(2) or 0)
total_minutes = hours * 60 + minutes
if total_minutes <= 0:
raise serializers.ValidationError(
"Dauer muss größer als 0 sein"
)
return timedelta(minutes=total_minutes)
# Verwendung in Serializer:
class MovieSerializer(serializers.ModelSerializer):
duration = DurationField()
class Meta:
model = Movie
fields = ['id', 'title', 'duration']
# API:
GET /api/movies/1/
{
"id": 1,
"title": "The Matrix",
"duration": "2h 16min" # ← Custom Format!
}
POST /api/movies/
{
"title": "New Movie",
"duration": "90min" # ← Wird zu timedelta
}
# filepath: movies/fields.py
class ColorField(serializers.Field):
"""Custom Field für Hex-Farben"""
def to_representation(self, value):
"""DB String → JSON Hex"""
if not value:
return None
# Sicherstellen dass # vorhanden
if not value.startswith('#'):
value = '#' + value
return value.upper()
def to_internal_value(self, data):
"""JSON Hex → DB String"""
if not data:
return None
# # entfernen falls vorhanden
color = data.replace('#', '')
# Validierung: Muss 6 Hex-Zeichen sein
import re
if not re.match(r'^[0-9A-Fa-f]{6}$', color):
raise serializers.ValidationError(
"Farbe muss Hex-Format haben: #RRGGBB oder RRGGBB"
)
return color.upper()
# Verwendung:
class GenreSerializer(serializers.ModelSerializer):
color = ColorField()
class Meta:
model = Genre
fields = ['id', 'name', 'color']
# API:
GET /api/genres/1/
{
"id": 1,
"name": "Action",
"color": "#FF0000" # ← Immer mit #, uppercase
}
POST /api/genres/
{
"name": "Comedy",
"color": "00ff00" # ← Akzeptiert verschiedene Formate
}
# Gespeichert als: "00FF00"
class EmailListField(serializers.Field):
"""Custom Field für Liste von E-Mails"""
def to_representation(self, value):
"""DB String → JSON Array"""
if not value:
return []
# Komma-separiert → Array
return [email.strip() for email in value.split(',')]
def to_internal_value(self, data):
"""JSON Array → DB String"""
if not data:
return ""
# Validierung
from django.core.validators import validate_email
from django.core.exceptions import ValidationError as DjangoValidationError
if not isinstance(data, list):
raise serializers.ValidationError(
"Muss eine Liste sein"
)
# Jede E-Mail validieren
validated_emails = []
for email in data:
try:
validate_email(email)
validated_emails.append(email.strip())
except DjangoValidationError:
raise serializers.ValidationError(
f"'{email}' ist keine gültige E-Mail"
)
# Array → Komma-separiert
return ','.join(validated_emails)
# Verwendung:
class MovieSerializer(serializers.ModelSerializer):
contact_emails = EmailListField()
class Meta:
model = Movie
fields = ['id', 'title', 'contact_emails']
# API:
GET /api/movies/1/
{
"id": 1,
"title": "The Matrix",
"contact_emails": [ # ← Array
"info@matrix.com",
"support@matrix.com"
]
}
POST /api/movies/
{
"title": "New Movie",
"contact_emails": [ # ← Array
"contact@example.com",
"admin@example.com"
]
}
# DB: "contact@example.com,admin@example.com"
to_representation() wird für jedes Objekt aufgerufen
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def to_representation(self, instance):
"""Response erweitern"""
# Standard-Daten holen
data = super().to_representation(instance)
# Meta-Informationen hinzufügen
data['_meta'] = {
'created': instance.created_at.isoformat(),
'updated': instance.updated_at.isoformat(),
'url': f'/movies/{instance.id}/',
'casting_count': instance.castings.count()
}
# Zusätzliche berechnete Felder
from datetime import datetime
current_year = datetime.now().year
data['age'] = current_year - instance.year
data['is_new'] = data['age'] < 5
return data
# Output:
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"rating": "8.7",
"age": 25,
"is_new": false,
"_meta": {
"created": "2024-01-01T10:00:00Z",
"updated": "2024-01-15T14:30:00Z",
"url": "/movies/1/",
"casting_count": 3
}
}
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def to_representation(self, instance):
"""Felder je nach Kontext anzeigen"""
data = super().to_representation(instance)
# Request aus Context holen
request = self.context.get('request')
if request and not request.user.is_staff:
# Normale User: Sensitive Daten entfernen
data.pop('internal_notes', None)
data.pop('production_cost', None)
# Leere Felder entfernen
data = {
key: value
for key, value in data.items()
if value is not None and value != ''
}
# Rating formatieren
if data.get('rating'):
data['rating_stars'] = '⭐' * int(float(data['rating']))
return data
# Output für normale User:
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"rating": "8.7",
"rating_stars": "⭐⭐⭐⭐⭐⭐⭐⭐"
# internal_notes und production_cost fehlen!
}
class MovieDetailSerializer(serializers.ModelSerializer):
castings = MovieCastingSerializer(many=True, read_only=True)
class Meta:
model = Movie
fields = '__all__'
def to_representation(self, instance):
"""Nur Lead-Actors anzeigen"""
data = super().to_representation(instance)
# Nur Hauptdarsteller
if 'castings' in data:
data['castings'] = [
casting for casting in data['castings']
if casting.get('is_lead')
]
# Zusätzlich: Top 3 nach Reihenfolge
data['castings'] = data['castings'][:3]
return data
# Output:
{
"id": 1,
"title": "The Matrix",
"castings": [ # Nur Top 3 Lead Actors!
{"id": 1, "artist": "Keanu Reeves", "role_name": "Neo"},
{"id": 2, "artist": "Laurence Fishburne", "role_name": "Morpheus"},
{"id": 3, "artist": "Carrie-Anne Moss", "role_name": "Trinity"}
]
}
to_internal_value() wird vor Validierung aufgerufen
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def to_internal_value(self, data):
"""Input normalisieren"""
# Kopie erstellen (nicht Original ändern!)
data = data.copy()
# Titel trimmen und kapitalisieren
if 'title' in data:
data['title'] = data['title'].strip()
# Ersten Buchstaben groß
if data['title']:
data['title'] = data['title'][0].upper() + data['title'][1:]
# Genre normalisieren
if 'genre' in data:
genre_mapping = {
'scifi': 'Sci-Fi',
'sci fi': 'Sci-Fi',
'action': 'Action',
'horror': 'Horror',
}
data['genre'] = genre_mapping.get(
data['genre'].lower(),
data['genre']
)
# Rating: String → Decimal
if 'rating' in data and isinstance(data['rating'], str):
data['rating'] = data['rating'].replace(',', '.')
# Standard-Verarbeitung
return super().to_internal_value(data)
# Input:
POST /api/movies/
{
"title": " the MATRIX ", # ← Unnötige Leerzeichen
"genre": "scifi", # ← Kleinbuchstaben
"rating": "8,7" # ← Komma statt Punkt
}
# Wird zu:
{
"title": "The MATRIX", # ← Getrimmt
"genre": "Sci-Fi", # ← Normalisiert
"rating": "8.7" # ← Punkt
}
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def to_internal_value(self, data):
"""Felder von externem API-Format konvertieren"""
data = data.copy()
# Externe API nutzt andere Feldnamen
field_mapping = {
'movie_title': 'title',
'release_year': 'year',
'imdb_rating': 'rating',
'category': 'genre',
}
# Felder umbenennen
for old_name, new_name in field_mapping.items():
if old_name in data:
data[new_name] = data.pop(old_name)
return super().to_internal_value(data)
# Input (externe API):
{
"movie_title": "The Matrix",
"release_year": 1999,
"imdb_rating": 8.7,
"category": "Sci-Fi"
}
# Wird zu (interne Felder):
{
"title": "The Matrix",
"year": 1999,
"rating": 8.7,
"genre": "Sci-Fi"
}
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def to_internal_value(self, data):
"""Intelligente Default-Werte"""
data = data.copy()
# Wenn Jahr fehlt, aktuelles Jahr
if 'year' not in data:
from datetime import datetime
data['year'] = datetime.now().year
# Wenn Genre fehlt, aus Titel raten
if 'genre' not in data and 'title' in data:
title_lower = data['title'].lower()
if any(word in title_lower for word in ['horror', 'scary', 'nightmare']):
data['genre'] = 'Horror'
elif any(word in title_lower for word in ['love', 'romance', 'heart']):
data['genre'] = 'Romance'
elif any(word in title_lower for word in ['space', 'future', 'alien']):
data['genre'] = 'Sci-Fi'
else:
data['genre'] = 'Drama' # Default
# User aus Request holen und als created_by setzen
request = self.context.get('request')
if request and hasattr(request, 'user'):
data['created_by'] = request.user.id
return super().to_internal_value(data)
# Input:
{
"title": "Alien from Space"
# Kein year, kein genre!
}
# Wird zu:
{
"title": "Alien from Space",
"year": 2024, # ← Aktuelles Jahr
"genre": "Sci-Fi", # ← Aus Titel erkannt
"created_by": 5 # ← Aus Request
}
Client entscheidet welche Felder er braucht
# filepath: movies/serializers.py
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
Basis-Serializer mit Dynamic Fields Support
Verwendung:
GET /api/movies/?fields=id,title,year
"""
def __init__(self, *args, **kwargs):
# Felder aus Request-Context holen
context = kwargs.get('context', {})
request = context.get('request')
# Standard-Initialisierung
super().__init__(*args, **kwargs)
if not request:
return
# 1. ?fields=id,title,year → Nur diese Felder
fields_param = request.query_params.get('fields')
if fields_param:
fields = fields_param.split(',')
# Alle anderen entfernen
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
# 2. ?exclude=description,notes → Diese ausschließen
exclude_param = request.query_params.get('exclude')
if exclude_param:
exclude = exclude_param.split(',')
for field_name in exclude:
self.fields.pop(field_name, None)
# Verwendung in Movie Serializer:
class MovieSerializer(DynamicFieldsModelSerializer):
castings = MovieCastingSerializer(many=True, read_only=True)
age = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = '__all__'
def get_age(self, obj):
from datetime import datetime
return datetime.now().year - obj.year
# 1. Alle Felder (Normal)
GET /api/movies/1/
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": "8.7",
"description": "...",
"age": 25,
"castings": [...]
}
# 2. Nur bestimmte Felder
GET /api/movies/1/?fields=id,title,year
{
"id": 1,
"title": "The Matrix",
"year": 1999
}
# 3. Felder ausschließen
GET /api/movies/1/?exclude=description,castings
{
"id": 1,
"title": "The Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": "8.7",
"age": 25
# description und castings fehlen!
}
# 4. Kombination
GET /api/movies/?fields=id,title,year,rating&exclude=description
# → Nur id, title, year, rating (exclude hat Vorrang)
Request, View, User, etc. an Serializer übergeben
# filepath: movies/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
class MovieListView(APIView):
def get(self, request):
movies = Movie.objects.all()
# Context explizit setzen
serializer = MovieSerializer(
movies,
many=True,
context={
'request': request, # Request-Objekt
'view': self, # View-Objekt
'format': 'json', # Format
'custom_data': 'value' # Eigene Daten!
}
)
return Response(serializer.data)
# ViewSets setzen Context automatisch!
from rest_framework import viewsets
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Context automatisch:
# {
# 'request': request,
# 'view': self,
# 'format': format
# }
class MovieSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
is_favorite = serializers.SerializerMethodField()
can_edit = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = '__all__'
def get_url(self, obj):
"""Absolute URL mit Request"""
request = self.context.get('request')
if request is None:
return None
# Reverse URL generieren
from django.urls import reverse
path = reverse('movie-detail', kwargs={'pk': obj.pk})
return request.build_absolute_uri(path)
def get_is_favorite(self, obj):
"""Ist Favorit für aktuellen User?"""
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return False
# Prüfen ob User diesen Film favorisiert hat
return obj.favorites.filter(user=request.user).exists()
def get_can_edit(self, obj):
"""Kann User diesen Film bearbeiten?"""
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return False
# Owner oder Admin
return (
obj.created_by == request.user or
request.user.is_staff
)
# Output:
GET /api/movies/1/
{
"id": 1,
"title": "The Matrix",
"url": "http://example.com/api/movies/1/", # ← Aus Request
"is_favorite": true, # ← Für diesen User
"can_edit": false # ← Für diesen User
}
# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def get_serializer_context(self):
"""Context erweitern"""
context = super().get_serializer_context()
# Custom Daten hinzufügen
context['include_stats'] = self.request.query_params.get('stats', False)
context['user_country'] = self.get_user_country()
context['api_version'] = 'v2'
return context
def get_user_country(self):
"""User-Land aus IP ermitteln (Beispiel)"""
# IP-Geolocation...
return 'DE'
# Serializer:
class MovieSerializer(serializers.ModelSerializer):
stats = serializers.SerializerMethodField()
available_in_country = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = '__all__'
def get_stats(self, obj):
"""Nur wenn ?stats=true"""
if not self.context.get('include_stats'):
return None
return {
'views': obj.view_count,
'likes': obj.like_count,
'comments': obj.comment_count
}
def get_available_in_country(self, obj):
"""Verfügbar in User-Land?"""
country = self.context.get('user_country')
return obj.available_countries.filter(code=country).exists()
# API:
GET /api/movies/1/?stats=true
{
"id": 1,
"title": "The Matrix",
"stats": { # ← Nur mit ?stats=true
"views": 1000000,
"likes": 50000,
"comments": 1500
},
"available_in_country": true # ← Basierend auf User-IP
}
❌ Schlecht:
class MovieListSerializer(serializers.ModelSerializer):
age = serializers.SerializerMethodField()
def get_age(self, obj):
from datetime import datetime
return datetime.now().year - obj.year
class MovieDetailSerializer(serializers.ModelSerializer):
age = serializers.SerializerMethodField()
def get_age(self, obj): # ← Duplikat!
from datetime import datetime
return datetime.now().year - obj.year
✅ Besser:
class MovieBaseSerializer(serializers.ModelSerializer):
"""Basis mit gemeinsamer Logik"""
age = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = ['id', 'title', 'year', 'age']
def get_age(self, obj):
from datetime import datetime
return datetime.now().year - obj.year
class MovieListSerializer(MovieBaseSerializer):
class Meta(MovieBaseSerializer.Meta):
fields = ['id', 'title', 'year']
class MovieDetailSerializer(MovieBaseSerializer):
castings = MovieCastingSerializer(many=True)
class Meta(MovieBaseSerializer.Meta):
fields = '__all__'
❌ Schlecht: Ein Serializer für alles
class MovieSerializer(serializers.ModelSerializer):
# Viel zu viel Logik in einem Serializer!
castings = ...
stats = ...
recommendations = ...
reviews = ...
# 500 Zeilen Code...
✅ Besser: Aufteilen
class MovieListSerializer(serializers.ModelSerializer):
"""Nur für Listen"""
class Meta:
model = Movie
fields = ['id', 'title', 'year', 'rating']
class MovieDetailSerializer(serializers.ModelSerializer):
"""Nur für Details"""
castings = MovieCastingSerializer(many=True)
class Meta:
model = Movie
fields = '__all__'
class MovieStatsSerializer(serializers.ModelSerializer):
"""Nur für Statistiken"""
stats = serializers.SerializerMethodField()
# ...
❌ Schlecht: N+1 Queries
class MovieSerializer(serializers.ModelSerializer):
casting_count = serializers.SerializerMethodField()
def get_casting_count(self, obj):
return obj.castings.count() # ← Query pro Movie!
✅ Besser: Annotation in View
class MovieViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Movie.objects.annotate(
casting_count_db=Count('castings')
)
class MovieSerializer(serializers.ModelSerializer):
casting_count = serializers.IntegerField(
source='casting_count_db' # ← Aus Annotation!
)
✅ Oder: Prefetch
class MovieViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Movie.objects.prefetch_related('castings')
class MovieSerializer(serializers.ModelSerializer):
casting_count = serializers.SerializerMethodField()
def get_casting_count(self, obj):
return obj.castings.count() # ← Schon geladen!
❌ Schlecht: Alle Felder schreibbar
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
# User kann ALLES ändern: created_by, is_verified, etc.
✅ Besser: read_only_fields
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
read_only_fields = [
'id',
'created_at',
'updated_at',
'created_by', # ← Nicht änderbar!
'is_verified', # ← Nur Admin
'view_count' # ← Nur System
]
✅ Oder: Separate Write-Serializer
class MovieWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = ['title', 'year', 'genre', 'rating']
# Nur diese Felder erlaubt!
❌ Schlecht: Validierung in View
class MovieCreateView(APIView):
def post(self, request):
if not request.data.get('title'):
return Response({'error': 'Title required'})
if request.data.get('year') < 1888:
return Response({'error': 'Year too early'})
# ...
✅ Besser: Validierung im Serializer
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
extra_kwargs = {
'title': {'required': True, 'min_length': 2}
}
def validate_year(self, value):
if value < 1888:
raise serializers.ValidationError("Year too early")
return value
def validate(self, attrs):
# Multi-field Validierung
if attrs['year'] < 1920 and attrs['rating'] > 9.5:
raise serializers.ValidationError("Invalid combination")
return attrs
✅ Gut dokumentiert:
class MovieSerializer(serializers.ModelSerializer):
"""
Serializer für Movie-Objekte.
Fields:
- id: Eindeutige ID (read-only)
- title: Film-Titel (required, min 2 chars)
- year: Erscheinungsjahr (1888-2100)
- rating: IMDB Rating (0-10, optional)
- age: Alter in Jahren (berechnet)
Nested:
- castings: Liste aller Besetzungen (read-only)
Validierung:
- Titel muss unique sein
- Jahr nicht in Zukunft
- Alte Filme max 9.5 Rating
"""
age = serializers.SerializerMethodField()
castings = MovieCastingSerializer(many=True, read_only=True)
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
def get_age(self, obj):
"""Berechne Alter des Films in Jahren"""
from datetime import datetime
return datetime.now().year - obj.year
Ein vollständiger, production-ready Serializer
# filepath: movies/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Movie(models.Model):
"""Film-Model"""
title = models.CharField(max_length=200, unique=True)
year = models.IntegerField()
genre = models.CharField(max_length=100)
rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True)
description = models.TextField(blank=True)
duration = models.DurationField(null=True, blank=True)
# Meta
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
# Stats
view_count = models.IntegerField(default=0)
like_count = models.IntegerField(default=0)
class Meta:
ordering = ['-year', 'title']
indexes = [
models.Index(fields=['year']),
models.Index(fields=['genre']),
]
def __str__(self):
return f"{self.title} ({self.year})"
class Artist(models.Model):
"""Künstler-Model"""
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class MovieCasting(models.Model):
"""Besetzung-Model"""
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings')
artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='castings')
role_name = models.CharField(max_length=200)
is_lead = models.BooleanField(default=False)
class Meta:
unique_together = ['movie', 'artist', 'role_name']
ordering = ['-is_lead', 'role_name']
def __str__(self):
return f"{self.artist} as {self.role_name} in {self.movie}"
# filepath: movies/validators.py
from rest_framework import serializers
def validate_year_range(value):
"""Jahr muss zwischen 1888 und 2100 liegen"""
if value < 1888:
raise serializers.ValidationError(
"Erstes Film war 1888 - Jahr zu früh!"
)
if value > 2100:
raise serializers.ValidationError(
f"Jahr {value} ist zu weit in der Zukunft!"
)
return value
def validate_rating_range(value):
"""Rating muss zwischen 0 und 10 liegen"""
if value is not None and (value < 0 or value > 10):
raise serializers.ValidationError(
f"Rating muss zwischen 0 und 10 liegen, nicht {value}"
)
return value
# filepath: movies/fields.py
from rest_framework import serializers
from datetime import timedelta
import re
class DurationField(serializers.Field):
"""Custom Field für Film-Dauer"""
def to_representation(self, value):
"""timedelta → String (z.B. "2h 16min")"""
if value is None:
return None
total_minutes = int(value.total_seconds() / 60)
hours = total_minutes // 60
minutes = total_minutes % 60
if hours > 0:
return f"{hours}h {minutes}min"
else:
return f"{minutes}min"
def to_internal_value(self, data):
"""String → timedelta"""
if data is None:
return None
# Parse "120min" oder "2h 30min"
pattern = r'(?:(\d+)h\s*)?(?:(\d+)min)?'
match = re.match(pattern, str(data))
if not match:
raise serializers.ValidationError(
"Format muss sein: '120min' oder '2h 30min'"
)
hours = int(match.group(1) or 0)
minutes = int(match.group(2) or 0)
total_minutes = hours * 60 + minutes
if total_minutes <= 0:
raise serializers.ValidationError("Dauer muss größer als 0 sein")
return timedelta(minutes=total_minutes)
# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting
from .validators import validate_year_range, validate_rating_range
from .fields import DurationField
from datetime import datetime
from django.db.models import Count
# ============================================
# BASE SERIALIZERS
# ============================================
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""Basis mit Dynamic Fields Support"""
def __init__(self, *args, **kwargs):
context = kwargs.get('context', {})
request = context.get('request')
super().__init__(*args, **kwargs)
if not request:
return
# ?fields=id,title
fields_param = request.query_params.get('fields')
if fields_param:
fields = fields_param.split(',')
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
# ?exclude=description
exclude_param = request.query_params.get('exclude')
if exclude_param:
exclude = exclude_param.split(',')
for field_name in exclude:
self.fields.pop(field_name, None)
# ============================================
# ARTIST SERIALIZERS
# ============================================
class ArtistSerializer(serializers.ModelSerializer):
"""Artist Serializer"""
full_name = serializers.SerializerMethodField()
age = serializers.SerializerMethodField()
class Meta:
model = Artist
fields = ['id', 'first_name', 'last_name', 'full_name', 'birth_date', 'age']
read_only_fields = ['id']
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}"
def get_age(self, obj):
if not obj.birth_date:
return None
today = datetime.now().date()
return today.year - obj.birth_date.year
# ============================================
# CASTING SERIALIZERS
# ============================================
class MovieCastingListSerializer(serializers.ModelSerializer):
"""Einfacher Casting Serializer für Listen"""
artist_name = serializers.CharField(source='artist.full_name', read_only=True)
class Meta:
model = MovieCasting
fields = ['id', 'artist_name', 'role_name', 'is_lead']
class MovieCastingDetailSerializer(serializers.ModelSerializer):
"""Detaillierter Casting Serializer"""
artist = ArtistSerializer(read_only=True)
# Für Write
artist_id = serializers.PrimaryKeyRelatedField(
source='artist',
queryset=Artist.objects.all(),
write_only=True
)
class Meta:
model = MovieCasting
fields = ['id', 'artist', 'artist_id', 'role_name', 'is_lead']
read_only_fields = ['id']
# ============================================
# MOVIE SERIALIZERS
# ============================================
class MovieListSerializer(DynamicFieldsModelSerializer):
"""
Movie List Serializer - Minimal für Übersichten
Features:
- Nur wichtigste Felder
- Berechnetes Alter
- Rating-Kategorie
- Dynamic Fields Support
"""
age = serializers.SerializerMethodField()
rating_category = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = [
'id', 'title', 'year', 'genre', 'rating',
'age', 'rating_category'
]
def get_age(self, obj):
"""Alter des Films"""
return datetime.now().year - obj.year
def get_rating_category(self, obj):
"""Rating-Kategorie"""
if not obj.rating:
return "Nicht bewertet"
rating = float(obj.rating)
if rating >= 9.0:
return "Meisterwerk"
elif rating >= 8.0:
return "Ausgezeichnet"
elif rating >= 7.0:
return "Gut"
elif rating >= 6.0:
return "Durchschnittlich"
else:
return "Schwach"
class MovieDetailSerializer(DynamicFieldsModelSerializer):
"""
Movie Detail Serializer - Maximal für Einzelansicht
Features:
- Alle Felder
- Nested Castings
- Berechnete Felder
- Stats
- Dynamic Fields Support
"""
# Nested Relations
castings = MovieCastingDetailSerializer(many=True, read_only=True)
created_by_name = serializers.CharField(
source='created_by.username',
read_only=True
)
# Custom Fields
duration = DurationField()
# SerializerMethodFields
age = serializers.SerializerMethodField()
is_classic = serializers.SerializerMethodField()
rating_category = serializers.SerializerMethodField()
lead_actors = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
# Stats (aus Context)
stats = serializers.SerializerMethodField()
class Meta:
model = Movie
fields = '__all__'
read_only_fields = [
'id', 'created_at', 'updated_at',
'created_by', 'view_count', 'like_count'
]
def get_age(self, obj):
return datetime.now().year - obj.year
def get_is_classic(self, obj):
return self.get_age(obj) > 30
def get_rating_category(self, obj):
if not obj.rating:
return "Nicht bewertet"
rating = float(obj.rating)
if rating >= 9.0: return "Meisterwerk"
elif rating >= 8.0: return "Ausgezeichnet"
elif rating >= 7.0: return "Gut"
elif rating >= 6.0: return "Durchschnittlich"
else: return "Schwach"
def get_lead_actors(self, obj):
"""Hauptdarsteller"""
lead_castings = obj.castings.filter(is_lead=True)[:3]
return [
{
'name': f"{c.artist.first_name} {c.artist.last_name}",
'role': c.role_name
}
for c in lead_castings
]
def get_url(self, obj):
"""Absolute URL"""
request = self.context.get('request')
if not request:
return None
from django.urls import reverse
path = reverse('movie-detail', kwargs={'pk': obj.pk})
return request.build_absolute_uri(path)
def get_stats(self, obj):
"""Stats nur wenn angefordert"""
if not self.context.get('include_stats'):
return None
return {
'views': obj.view_count,
'likes': obj.like_count,
'casting_count': obj.castings.count()
}
class MovieWriteSerializer(serializers.ModelSerializer):
"""
Movie Write Serializer - Nur für Create/Update
Features:
- Nur beschreibbare Felder
- Umfassende Validierung
- Custom Fields
- Normalisierung
"""
# Custom Validators
year = serializers.IntegerField(validators=[validate_year_range])
rating = serializers.DecimalField(
max_digits=3,
decimal_places=1,
validators=[validate_rating_range],
required=False,
allow_null=True
)
# Custom Fields
duration = DurationField(required=False, allow_null=True)
class Meta:
model = Movie
fields = ['title', 'year', 'genre', 'rating', 'description', 'duration']
extra_kwargs = {
'title': {
'required': True,
'min_length': 2,
'max_length': 200
},
'description': {
'required': False,
'allow_blank': True
}
}
def validate_title(self, value):
"""Titel validieren"""
# Trimmen
value = value.strip()
# Unique check (bei Update eigenen Film ausschließen)
queryset = Movie.objects.filter(title__iexact=value)
if self.instance:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise serializers.ValidationError(
f"Film mit Titel '{value}' existiert bereits!"
)
return value
def validate_genre(self, value):
"""Genre validieren"""
allowed_genres = [
'Action', 'Comedy', 'Drama', 'Horror',
'Sci-Fi', 'Romance', 'Thriller', 'Documentary'
]
if value and value not in allowed_genres:
raise serializers.ValidationError(
f"Genre muss eines sein von: {', '.join(allowed_genres)}"
)
return value
def validate(self, attrs):
"""Object-level Validierung"""
year = attrs.get('year')
rating = attrs.get('rating')
# Jahr nicht in Zukunft
current_year = datetime.now().year
if year and year > current_year + 1:
raise serializers.ValidationError({
'year': f'Jahr darf nicht mehr als {current_year + 1} sein'
})
# Alte Filme max 9.5
if year and year < 1920 and rating and rating > 9.5:
raise serializers.ValidationError(
'Stummfilme vor 1920 können maximal 9.5 Rating haben'
)
return attrs
def to_internal_value(self, data):
"""Input normalisieren"""
data = data.copy()
# Titel normalisieren
if 'title' in data:
data['title'] = data['title'].strip()
# Genre normalisieren
if 'genre' in data:
genre_mapping = {
'scifi': 'Sci-Fi',
'sci fi': 'Sci-Fi',
}
data['genre'] = genre_mapping.get(
data['genre'].lower(),
data['genre']
)
return super().to_internal_value(data)
def create(self, validated_data):
"""Custom Create"""
# User aus Context
request = self.context.get('request')
if request and hasattr(request, 'user'):
validated_data['created_by'] = request.user
return super().create(validated_data)
# filepath: movies/views.py
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Prefetch
from .models import Movie, Artist, MovieCasting
from .serializers import (
MovieListSerializer,
MovieDetailSerializer,
MovieWriteSerializer,
ArtistSerializer,
MovieCastingDetailSerializer
)
class MovieViewSet(viewsets.ModelViewSet):
"""
ViewSet für Movies mit allen Features
Features:
- Verschiedene Serializers für List/Detail/Write
- Filtering & Search
- Ordering
- Performance-Optimierung
- Custom Actions
- Dynamic Fields
- Stats on-demand
"""
queryset = Movie.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly]
# Filtering
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
filterset_fields = ['year', 'genre']
search_fields = ['title', 'description']
ordering_fields = ['year', 'rating', 'title', 'created_at']
ordering = ['-year']
def get_queryset(self):
"""QuerySet optimieren"""
queryset = Movie.objects.all()
# Action-spezifische Optimierung
if self.action == 'list':
# Liste: Nur nötige Felder
queryset = queryset.only(
'id', 'title', 'year', 'genre', 'rating'
)
elif self.action == 'retrieve':
# Detail: Alles + Related
queryset = queryset.select_related(
'created_by'
).prefetch_related(
Prefetch(
'castings',
queryset=MovieCasting.objects.select_related('artist')
)
)
# Custom Filters
year_min = self.request.query_params.get('year_min')
if year_min:
queryset = queryset.filter(year__gte=year_min)
year_max = self.request.query_params.get('year_max')
if year_max:
queryset = queryset.filter(year__lte=year_max)
return queryset
def get_serializer_class(self):
"""Dynamisch Serializer wählen"""
if self.action == 'list':
return MovieListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return MovieWriteSerializer
else:
return MovieDetailSerializer
def get_serializer_context(self):
"""Context erweitern"""
context = super().get_serializer_context()
# Stats on-demand
context['include_stats'] = self.request.query_params.get(
'stats',
'false'
).lower() == 'true'
return context
@action(detail=True, methods=['post'])
def add_casting(self, request, pk=None):
"""Custom Action: Casting hinzufügen"""
movie = self.get_object()
serializer = MovieCastingDetailSerializer(
data=request.data,
context={'request': request}
)
if serializer.is_valid():
serializer.save(movie=movie)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['get'])
def stats(self, request, pk=None):
"""Custom Action: Detaillierte Stats"""
movie = self.get_object()
return Response({
'views': movie.view_count,
'likes': movie.like_count,
'castings': {
'total': movie.castings.count(),
'leads': movie.castings.filter(is_lead=True).count()
},
'age': 2024 - movie.year,
'rating': str(movie.rating) if movie.rating else None
})
@action(detail=False, methods=['get'])
def top_rated(self, request):
"""Custom Action: Top-bewertete Filme"""
top_movies = self.get_queryset().filter(
rating__isnull=False
).order_by('-rating')[:10]
serializer = self.get_serializer(top_movies, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def recent(self, request):
"""Custom Action: Neueste Filme"""
recent_movies = self.get_queryset().order_by('-year', '-created_at')[:20]
serializer = self.get_serializer(recent_movies, many=True)
return Response(serializer.data)
class ArtistViewSet(viewsets.ModelViewSet):
"""ViewSet für Artists"""
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['first_name', 'last_name']
ordering_fields = ['last_name', 'birth_date']
ordering = ['last_name']
~50 Zeilen Code
class MovieSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
year = serializers.IntegerField()
rating = serializers.DecimalField(
max_digits=3,
decimal_places=1
)
def create(self, validated_data):
return Movie.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.year = validated_data.get('year', instance.year)
instance.rating = validated_data.get('rating', instance.rating)
instance.save()
return instance
✅ Vorteile:
❌ Nachteile:
~5 Zeilen Code
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['id', 'created_at']
# Fertig! create() und update() automatisch!
✅ Vorteile:
❌ Nachteile:
~20 Zeilen Code
class MovieSerializer(serializers.ModelSerializer):
age = serializers.SerializerMethodField()
castings = MovieCastingSerializer(many=True, read_only=True)
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['id', 'created_at']
extra_kwargs = {
'title': {'min_length': 2}
}
def get_age(self, obj):
return datetime.now().year - obj.year
def validate_year(self, value):
if value < 1888:
raise serializers.ValidationError("Too early")
return value
# Beste Balance!
✅ Vorteile:
⭐ Empfohlen für 95% der Fälle!
# SCHLECHT:
class MovieSerializer(serializers.ModelSerializer):
casting_count = serializers.SerializerMethodField()
def get_casting_count(self, obj):
return obj.castings.count() # ← Query pro Movie!
# Bei 100 Movies = 101 Queries! 😱
# BESSER:
# View:
def get_queryset(self):
return Movie.objects.annotate(
casting_count_db=Count('castings')
)
# Serializer:
casting_count = serializers.IntegerField(
source='casting_count_db'
)
# Nur 1 Query! 🚀
# SCHLECHT:
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
# User kann ALLES ändern: created_by, is_verified, etc.!
# BESSER:
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
read_only_fields = [
'id', 'created_at', 'updated_at',
'created_by', 'is_verified', 'view_count'
]
# ODER: Separate Write-Serializer
class MovieWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = ['title', 'year', 'genre', 'rating']
# SCHLECHT:
class MovieCreateView(APIView):
def post(self, request):
if not request.data.get('title'):
return Response({'error': 'Title required'}, status=400)
if request.data.get('year') < 1888:
return Response({'error': 'Year too early'}, status=400)
# ... viel Code
# BESSER:
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
extra_kwargs = {
'title': {'required': True}
}
def validate_year(self, value):
if value < 1888:
raise serializers.ValidationError("Year too early")
return value
# View wird einfach:
class MovieCreateView(APIView):
def post(self, request):
serializer = MovieSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
# SCHLECHT:
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
depth = 3 # ← Zu tief! Huge Response!
# Response wird riesig und kann Endlos-Schleifen haben!
# BESSER:
# Keine depth, sondern explizite Nested Serializers
castings = MovieCastingSerializer(many=True, read_only=True)
Validierung & Transformationen prüfen
# filepath: movies/tests/test_serializers.py
from django.test import TestCase
from rest_framework.exceptions import ValidationError
from datetime import datetime
from movies.models import Movie, Artist, MovieCasting
from movies.serializers import MovieSerializer, MovieWriteSerializer
class MovieSerializerTest(TestCase):
"""Tests für Movie Serializer"""
def setUp(self):
"""Test-Daten erstellen"""
self.movie = Movie.objects.create(
title="The Matrix",
year=1999,
genre="Sci-Fi",
rating=8.7
)
def test_serialization(self):
"""Test: Serialisierung (Python → JSON)"""
serializer = MovieSerializer(self.movie)
data = serializer.data
# Assertions
self.assertEqual(data['title'], "The Matrix")
self.assertEqual(data['year'], 1999)
self.assertEqual(data['genre'], "Sci-Fi")
self.assertEqual(float(data['rating']), 8.7)
# Berechnete Felder
current_year = datetime.now().year
expected_age = current_year - 1999
self.assertEqual(data['age'], expected_age)
def test_deserialization(self):
"""Test: Deserialisierung (JSON → Python)"""
data = {
'title': 'Inception',
'year': 2010,
'genre': 'Sci-Fi',
'rating': 8.8
}
serializer = MovieWriteSerializer(data=data)
self.assertTrue(serializer.is_valid())
movie = serializer.save()
self.assertEqual(movie.title, "Inception")
self.assertEqual(movie.year, 2010)
def test_validation_year_too_early(self):
"""Test: Jahr zu früh"""
data = {
'title': 'Old Movie',
'year': 1800, # ← Zu früh!
'genre': 'Drama'
}
serializer = MovieWriteSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn('year', serializer.errors)
def test_validation_title_duplicate(self):
"""Test: Titel muss unique sein"""
data = {
'title': 'The Matrix', # ← Existiert schon!
'year': 2020,
'genre': 'Action'
}
serializer = MovieWriteSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn('title', serializer.errors)
def test_update(self):
"""Test: Update funktioniert"""
data = {
'title': 'The Matrix Reloaded',
'year': 2003,
'genre': 'Sci-Fi',
'rating': 7.2
}
serializer = MovieWriteSerializer(self.movie, data=data)
self.assertTrue(serializer.is_valid())
updated_movie = serializer.save()
self.assertEqual(updated_movie.id, self.movie.id)
self.assertEqual(updated_movie.title, "The Matrix Reloaded")
self.assertEqual(updated_movie.year, 2003)
JWT Tokens, Session Auth
Custom Permissions
django-filter
Custom FilterSets
ImageField, FileField
Media Storage
Rate Limiting
Redis Cache
API Tests
Docker Setup
Implementiere eigene API mit allem was du gelernt hast!
Keep coding, keep learning! 💻